﻿#region File and License Information

/*
<File>
	<License>
		Copyright © 2009 - 2017, Daniel Vaughan. All rights reserved.
		This file is part of Calcium (http://calciumsdk.net), 
		which is released under the MIT License.
		See file /Documentation/License.txt for details.
	</License>
	<CreationDate>2012-02-18 13:11:43Z</CreationDate>
</File>
*/

#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;

using Outcoder.Collections;
using Outcoder.Concurrency;
using Outcoder.Reflection;

namespace Outcoder.InversionOfControl
{
	/// <summary>
	/// This <c>IContainer</c> supports, singleton and non-singleton
	/// mapping from concrete types and interface types to concrete types,
	/// object instances, lambda expressions. 
	/// It also supports dependency injection.
	/// Unit tests can be found in the WPF unit test library.
	/// </summary>
	public class SimpleContainer : IContainer
	{
		readonly Dictionary<Type, ResolverDictionary> typeDictionary = new Dictionary<Type, ResolverDictionary>();
		readonly Dictionary<string, Type> keyDictionary = new Dictionary<string, Type>();

		static readonly string defaultKey = Guid.NewGuid().ToString();
		readonly ReaderWriterLockSlim lockSlim = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

		readonly Dictionary<Type, List<string>> cycleDictionary = new Dictionary<Type, List<string>>();

		readonly Dictionary<Type, ConstructorInvokeInfo> constructorDictionary
			= new Dictionary<Type, ConstructorInvokeInfo>();
		readonly Dictionary<Type, List<PropertyInfo>> injectablePropertyDictionary
								= new Dictionary<Type, List<PropertyInfo>>();
		readonly Dictionary<string, Action<object, object>> propertyActionDictionary
			= new Dictionary<string, Action<object, object>>();
		readonly Dictionary<Type, PropertyInfo[]> propertyDictionary
			= new Dictionary<Type, PropertyInfo[]>();

		readonly ReaderWriterLockSlim constructorDictionaryLockSlim
			= new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

		/// <summary>
		/// Prevents multiple threads from creating more 
		/// than one singleton instance.
		/// Decreases performance if <c>true</c>.
		/// Default value is <c>false</c>.
		/// </summary>
		public bool ThreadSafe { get; set; }

		/// <summary>
		/// If <c>true</c> the container resolves values for properties 
		/// that are decorated with a <seealso cref="InjectDependenciesAttribute"/>.
		/// There's a performance penalty for that. If you do not use
		/// <c>InjectDependenciesAttribute</c> for properties, 
		/// set this property to false to improve performance.
		/// Default value is <c>true</c>.
		/// </summary>
		public bool PropertyInjectionAttributesEnabled { get; set; } = true;

		sealed class ResolverDictionary : Dictionary<string, Resolver>
		{
		}

		sealed class Resolver
		{
			public Func<object> CreateInstanceFunc { get; set; }
			public object Instance;
			public bool Singleton;

			public object GetObject()
			{
				if (Singleton)
				{
					if (Instance != null)
					{
						return Instance;
					}

					Instance = CreateInstanceFunc();

					if (Instance != null)
					{
						CreateInstanceFunc = null;
					}

					return Instance;
				}

				return CreateInstanceFunc();
			}
		}

		public void Register<TFrom, TTo>(bool singleton = false, string key = null) where TTo : TFrom
		{
			Type fromType = typeof(TFrom);
			Type toType = typeof(TTo);
			Register(fromType, toType, singleton, key);
		}

		public void Register(Type fromType, Type toType, 
			bool singleton = false, string key = null)
		{
			key = GetKeyValueOrDefault(key);

			bool useLock = ThreadSafe;
			if (useLock)
			{
				lockSlim.EnterWriteLock();
			}

			try
			{
				ResolverDictionary resolverDictionary;
				if (!typeDictionary.TryGetValue(fromType, out resolverDictionary))
				{
					resolverDictionary = new ResolverDictionary();
					typeDictionary[fromType] = resolverDictionary;
				}

				Resolver resolver = new Resolver
				{
					CreateInstanceFunc = () => Instantiate(toType),
					Singleton = singleton
				};
				resolverDictionary[key] = resolver;
				keyDictionary[key] = fromType;
			}
			finally
			{
				if (useLock)
				{
					lockSlim.ExitWriteLock();
				}
			}
		}

		public void Register<TInterface>(TInterface instance, string key = null)
		{
			key = GetKeyValueOrDefault(key);
			Type type = typeof(TInterface);

			bool useLock = ThreadSafe;
			if (useLock)
			{
				lockSlim.EnterWriteLock();
			}

			try
			{
				ResolverDictionary value;
				if (!typeDictionary.TryGetValue(type, out value))
				{
					value = new ResolverDictionary();
				}

				value[key] = new Resolver { CreateInstanceFunc = () => instance };
				typeDictionary[type] = value;
			}
			finally
			{
				if (useLock)
				{
					lockSlim.ExitWriteLock();
				}
			}
		}

		public void Register<T>(Func<T> getInstanceFunc, bool singleton = false, string key = null)
		{
			ArgumentValidator.AssertNotNull(getInstanceFunc, "getInstanceFunc");

			key = GetKeyValueOrDefault(key);
			Type type = typeof(T);

			bool useLock = ThreadSafe;
			if (useLock)
			{
				lockSlim.EnterWriteLock();
			}

			try
			{
				ResolverDictionary resolverDictionary;
				if (!typeDictionary.TryGetValue(type, out resolverDictionary))
				{
					resolverDictionary = new ResolverDictionary();
				}

				Resolver resolver = new Resolver { Singleton = singleton };

				Func<object> getObjectFunc = () =>
				{
					List<string> keys;
					if (cycleDictionary.TryGetValue(type, out keys))
					{
						if (keys.Contains(key))
						{
							throw new ResolutionException($"Cycle detected for {type} with key \"{key}\"");
						}

						keys.Add(key);
					}
					else
					{
						keys = new List<string> { key };
						cycleDictionary.Add(type, keys);
					}

					T result;
					try
					{
						result = getInstanceFunc();
					}
					finally
					{
						keys.Remove(key);
					}

					if (resolver.Singleton)
					{
						resolver.CreateInstanceFunc = null;
						resolver.Instance = result;
					}

					return result;
				};

				resolver.CreateInstanceFunc = getObjectFunc;
				
				resolverDictionary[key] = resolver;
				keyDictionary[key] = type;

				typeDictionary[type] = resolverDictionary;
			}
			finally
			{
				if (useLock)
				{
					lockSlim.ExitWriteLock();
				}
			}
		}

		public void Register(Type type, Func<object> getInstanceFunc, bool singleton = false, string key = null)
		{
			ArgumentValidator.AssertNotNull(type, "fromType");
			ArgumentValidator.AssertNotNull(getInstanceFunc, "getInstanceFunc");

			key = GetKeyValueOrDefault(key);

			bool useLock = ThreadSafe;
			if (useLock)
			{
				lockSlim.EnterWriteLock();
			}

			try
			{
				ResolverDictionary resolverDictionary;
				if (!typeDictionary.TryGetValue(type, out resolverDictionary))
				{
					resolverDictionary = new ResolverDictionary();
				}

				Resolver resolver = new Resolver { Singleton = singleton };

				Func<object> getObjectFunc = () =>
				{
					var result = getInstanceFunc();

					if (resolver.Singleton)
					{
						resolver.CreateInstanceFunc = null;
						resolver.Instance = result;
					}

					return result;
				};

				resolver.CreateInstanceFunc = getObjectFunc;
				
				resolverDictionary[key] = resolver;
				keyDictionary[key] = type;

				typeDictionary[type] = resolverDictionary;
			}
			finally
			{
				if (useLock)
				{
					lockSlim.ExitWriteLock();
				}
			}
		}

		public void Register(Type type, object instance, string key = null)
		{
			ArgumentValidator.AssertNotNull(type, "type");
			key = GetKeyValueOrDefault(key);

			bool useLock = ThreadSafe;
			if (useLock)
			{
				lockSlim.EnterWriteLock();
			}

			try
			{
				ResolverDictionary value;
				if (!typeDictionary.TryGetValue(type, out value))
				{
					value = new ResolverDictionary();
				}

				Resolver info = new Resolver { CreateInstanceFunc = () => instance };

				value[key] = info;

				typeDictionary[type] = value;
			}
			finally
			{
				if (useLock)
				{
					lockSlim.ExitWriteLock();
				}
			}
		}

		public bool IsRegistered<T>(string key = null)
		{
			return IsRegistered(typeof(T), key);
		}

		public bool IsRegistered(Type fromType, string key = null)
		{
			bool useLock = ThreadSafe;
			if (useLock)
			{
				lockSlim.EnterReadLock();
			}

			try
			{
				if (!string.IsNullOrEmpty(key))
				{
					/* Improve performance of this by using a lookup dictionary. */
					foreach (KeyValuePair<Type, ResolverDictionary> pair in typeDictionary)
					{
						if (!fromType.IsAssignableFrom(pair.Key))
						{
							continue;
						}

						var funcDictionary = pair.Value;
						if (pair.Value == null)
						{
							continue;
						}

						if (funcDictionary.ContainsKey(key))
						{
							return true;
						}
					}
				}

				ResolverDictionary dictionary;
				if (typeDictionary.TryGetValue(fromType, out dictionary) && dictionary != null)
				{
					if (dictionary.ContainsKey(key ?? defaultKey))
					{
						return true;
					}
				}
			}
			finally
			{
				if (useLock)
				{
					lockSlim.ExitReadLock();
				}
			}

			return false;
		}

		public T Resolve<T>(string key = null)
		{
			return (T)ResolveAux(typeof(T), key);
		}

		public object Resolve(Type type, string key = null)
		{
			ArgumentValidator.AssertNotNull(type, "type");
			return ResolveAux(type, key);
		}

		object ResolveAux(Type type, string key = null, Dictionary<string, object> resolvedObjects = null)
		{
			bool alreadyResolved = false;
			object result = null;
			if (resolvedObjects != null && key != null)
			{
				if (resolvedObjects.TryGetValue(key, out result))
				{
					alreadyResolved = true;
				}
			}

			if (!alreadyResolved)
			{
				result = ResolveCore(type, key);

				if (key != null && resolvedObjects != null)
				{
					resolvedObjects[key] = result;
				}

				if (result != null && PropertyInjectionAttributesEnabled)
				{
					ResolveProperties(result, resolvedObjects);
				}
			}

			if (resolvedObjects != null)
			{
				resolvedObjects.Clear();
			}

			return result;
		}

		public IEnumerable<object> ResolveAll(string key)
		{
			ArgumentValidator.AssertNotNull(key, "key");

			List<object> result = new List<object>();
			
			foreach (KeyValuePair<Type, ResolverDictionary> pair in typeDictionary)
			{
				var dictionary = pair.Value;
				Resolver resolver;
				if (dictionary.TryGetValue(key, out resolver))
				{
					var item = resolver.Instance ?? resolver.CreateInstanceFunc?.Invoke();
					result.Add(item);
				}
			}

			return result;
		}

		public IEnumerable<Object> ResolveAll(Type fromType)
		{
			ArgumentValidator.AssertNotNull(fromType, "fromType");
			var list = new List<object>();

			ResolverDictionary resolverDictionary;
			bool retrievedValue;

			bool useLock = ThreadSafe;
			if (useLock)
			{
				lockSlim.EnterReadLock();
			}

			try
			{
				retrievedValue = typeDictionary.TryGetValue(fromType, out resolverDictionary);
			}
			finally
			{
				if (useLock)
				{
					lockSlim.ExitReadLock();
				}
			}

			if (!retrievedValue)
			{
				return list;
			}

			if (resolverDictionary == null)
			{
				object builtObject = BuildUp(fromType, null);
				if (builtObject != null)
				{
					list.Add(builtObject);
				}
			}
			else
			{
				foreach (Resolver resolver in resolverDictionary.Values)
				{
					var item = resolver.GetObject();
					list.Add(item);
				}
			}

			return list;
		}

		public IEnumerable<TFrom> ResolveAll<TFrom>() where TFrom : class
		{
			Type fromType = typeof(TFrom);
			List<TFrom> list = new List<TFrom>();

			ResolverDictionary dictionary;
			bool retrieved;

			bool useLock = ThreadSafe;
			if (useLock)
			{
				lockSlim.EnterReadLock();
			}

			try
			{
				retrieved = typeDictionary.TryGetValue(fromType, out dictionary);
			}
			finally
			{
				if (useLock)
				{
					lockSlim.ExitReadLock();
				}
			}
			
			if (!retrieved)
			{
				return list;
			}

			//			if (dictionary == null)
			//			{
			//#if NETFX_CORE
			//				if (!fromType.GetTypeInfo().IsInterface)
			//#else
			//				if (!fromType.IsInterface)
			//#endif
			//				{
			//					foreach (var pair in singletonTypes)
			//					{
			//						var typeKey = pair.Key;
			//						if (typeKey.ObjectType == fromType)
			//						{
			//							Type toType = pair.Value;
			//							object o = BuildUp(toType, typeKey.Key);
			//							if (o != null)
			//							{
			//								list.Add((TFrom)o);
			//							}
			//						}
			//					}
			//				}
			//			}
			//			else
			//			{
			//				list.AddRange(dictionary.Values.Select(resolveFunc => resolveFunc.GetObject()));
			//			}

			//list.AddRange(dictionary.Values.Select(resolveFunc => resolveFunc.GetObject()));

			foreach (var resolver in dictionary.Values)
			{
				var item = (TFrom)resolver.GetObject();
				list.Add(item);
			}

			return list;
		}

		object ResolveCore(Type type, string key)
		{
			key = GetKeyValueOrDefault(key);

			if (type == null)
			{
				type = ResolveType(key);

				if (type == null)
				{
					throw new ResolutionException("Failed to resolve type for " + key);
				}
			}
			
			ResolverDictionary resolvers;
			Resolver resolver = null;

			bool useLock = ThreadSafe;
			if (useLock)
			{
				lockSlim.EnterReadLock();
			}

			try
			{
				if (typeDictionary.TryGetValue(type, out resolvers) && resolvers != null)
				{
					/* If the ResolveProperties method calls through here, it may be searching for a registration per
					 * strongly-typed name that does not exist. In this case, just get the default registration. */
					if (!resolvers.TryGetValue(key, out resolver))
					{
						key = GetKeyValueOrDefault(null);
						resolver = resolvers[key];
					}
				}
			}
			finally
			{
				if (useLock)
				{
					lockSlim.ExitReadLock();
				}
			}

			if (resolvers == null)
			{
				return BuildUp(type, key);
			}

			if (resolver != null)
			{
				return resolver.GetObject();
			}

			return BuildUp(type, key);
		}

		Type ResolveType(string key)
		{
			Type result = GetTypeFromContainer(key);
			if (result != null)
			{
				return result;
			}

			/*  Not in the container? Try the Assembly. */
#if NETFX_CORE
			return GetType().GetTypeInfo().Assembly.GetType(key);
#else
			return Assembly.GetExecutingAssembly().GetTypes().SingleOrDefault(t => t.Name == key);
#endif
		}

		Type GetTypeFromContainer(string key)
		{
			Type result;
			if (!keyDictionary.TryGetValue(key, out result))
			{
				return null;
			}

			return result;
		}

		void ResolveProperties(object instance, Dictionary<string, object> resolvedObjects)
		{
			Type type = instance.GetType();
			List<PropertyInfo> injectableProperties;

			bool useLock = ThreadSafe;
			if (useLock)
			{
				lockSlim.EnterReadLock();
			}

			try
			{
				bool hasCachedInjectableProperties = injectablePropertyDictionary.TryGetValue(type, out injectableProperties);

				if (!hasCachedInjectableProperties)
				{
					PropertyInfo[] properties;
					bool hasCachedProperties = propertyDictionary.TryGetValue(type, out properties);

					if (!hasCachedProperties)
					{
#if NETFX_CORE
						properties = type.GetTypeInfo().DeclaredProperties.ToArray();
#else
						properties = type.GetProperties();
#endif
						propertyDictionary[type] = properties;
					}

					injectableProperties = new List<PropertyInfo>();

					foreach (PropertyInfo propertyInfo in properties)
					{
						var dependencyAttributes = propertyInfo.GetCustomAttributes(typeof(InjectDependenciesAttribute), false);
#if NETFX_CORE
						if (!dependencyAttributes.Any())
#else
						if (dependencyAttributes.Length < 1) /* Faster than using LINQ. */
#endif
						{
							continue;
						}

						injectableProperties.Add(propertyInfo);
					}

					injectablePropertyDictionary[type] = injectableProperties;
				}
			}
			finally
			{
				if (useLock)
				{
					lockSlim.ExitReadLock();
				}
			}

			if (injectableProperties == null || injectableProperties.Count < 1)
			{
				return;
			}

			if (resolvedObjects == null)
			{
				resolvedObjects = new Dictionary<string, object>();
			}

			string typeName = type.FullName + ".";

			foreach (PropertyInfo propertyInfo in injectableProperties)
			{
				string fullPropertyName = typeName + propertyInfo.Name;

				object propertyValue = ResolveAux(propertyInfo.PropertyType, fullPropertyName, resolvedObjects);

				Action<object, object> setter;

				if (!propertyActionDictionary.TryGetValue(fullPropertyName, out setter))
				{
					setter = ReflectionCompiler.CreateSetter(propertyInfo);
					propertyActionDictionary[fullPropertyName] = setter;
				}

				setter(instance, propertyValue);
			}
		}
		
		object BuildUp(Type type, string key)
		{
			if (type == null)
			{
				throw new ResolutionException("type should not be null.");
			}

			object instance = null;

			bool useLock = ThreadSafe;
			if (useLock)
			{
				lockSlim.EnterReadLock();
			}

			try
			{
				ResolverDictionary resolverDictionary;
				if (typeDictionary.TryGetValue(type, out resolverDictionary))
				{
					Resolver resolver;
					if (resolverDictionary.TryGetValue(key, out resolver))
					{
						instance = resolver.GetObject();
					}
				}
			}
			finally
			{
				if (useLock)
				{
					lockSlim.ExitReadLock();
				}
			}

			if (instance != null)
			{
				return instance;
			}

			return Instantiate(type);
		}

		class ConstructorInvokeInfo
		{
			internal readonly ParameterInfo[] ParameterInfos;

			Func<object[], object> constructorFunc;

			internal Func<object[], object> ConstructorFunc
			{
				get
				{
					if (constructorFunc == null)
					{
						constructorFunc = ReflectionCompiler.CreateFunc(Constructor);
					}

					return constructorFunc;
				}
			}

			internal readonly ConstructorInfo Constructor;

			internal ConstructorInvokeInfo(ConstructorInfo constructor)
			{
				Constructor = constructor;
				ParameterInfos = constructor.GetParameters();
			}
		}

		object Instantiate(ConstructorInvokeInfo info)
		{
			var constructorParameters = info.ParameterInfos;
			var length = constructorParameters.Length;
			var parametersList = new object[length];
			for (int i = 0; i < length; i++)
			{
				ParameterInfo parameterInfo = constructorParameters[i];
				object parameter = Resolve(parameterInfo.ParameterType);
				if (parameter == null && !parameterInfo.IsOptional)
				{
					throw new ResolutionException(
						"Failed to instantiate parameter " + parameterInfo.Name);
				}

				parametersList[i] = parameter;
			}

			var constructorFunc = info.ConstructorFunc;
			try
			{
				//return info.Constructor.Invoke(parametersList.ToArray());
				return constructorFunc(parametersList);
			}
			catch (Exception ex)
			{
				throw new ResolutionException("Failed to resolve " + info.Constructor.DeclaringType, ex);
			}
		}

		object Instantiate(Type type)
		{
			ConstructorInvokeInfo invokeInfo;

			bool useLock = ThreadSafe;
			if (useLock)
			{
				constructorDictionaryLockSlim.EnterReadLock();
			}

			try
			{
				if (constructorDictionary.TryGetValue(type, out invokeInfo))
				{
					return Instantiate(invokeInfo);
				}
			}
			finally
			{
				if (useLock)
				{
					constructorDictionaryLockSlim.ExitReadLock();
				}
			}

#if NETFX_CORE
			var constructors = type.GetTypeInfo().DeclaredConstructors.Where(x => !x.IsStatic && x.IsPublic).ToArray();
#else
			var constructors = type.GetConstructors();
#endif

			ConstructorInfo constructor = null;

			if (constructors.Length > 0)
			{
				ConstructorInfo bestMatch = null;
				int biggestLength = -1;

				foreach (ConstructorInfo constructorInfo in constructors)
				{
					var dependencyAttributes = constructorInfo.GetCustomAttributes(typeof(InjectDependenciesAttribute), false);
#if NETFX_CORE
					var attributeCount = dependencyAttributes.Count();
#else
					var attributeCount = dependencyAttributes.Length;
#endif
					bool hasAttribute = attributeCount > 0;

					if (hasAttribute)
					{
						constructor = constructorInfo;
						break;
					}

					var length = constructorInfo.GetParameters().Length;

					if (length > biggestLength)
					{
						biggestLength = length;
						bestMatch = constructorInfo;
					}
				}

				if (constructor == null)
				{
					constructor = bestMatch;
				}
			}
			else
			{
#if !NETFX_CORE
				ConstructorInfo bestMatch = null;
				int biggestLength = -1;

				constructors = type.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic);

				if (constructors.Length > 0)
				{
					foreach (ConstructorInfo constructorInfo in constructors)
					{
						if (constructorInfo.GetCustomAttributes(typeof(InjectDependenciesAttribute), false).Length > 0)
						{
							constructor = constructorInfo;
							break;
						}

						var length = constructorInfo.GetParameters().Length;

						if (length > biggestLength)
						{
							biggestLength = length;
							bestMatch = constructorInfo;
						}
					}

					if (constructor == null)
					{
						constructor = bestMatch;
					}
				}
#endif
			}

			if (constructor == null)
			{
				throw new ResolutionException(
					"Could not locate a constructor for " + type.FullName);
			}

			invokeInfo = new ConstructorInvokeInfo(constructor);

			if (useLock)
			{
				constructorDictionaryLockSlim.EnterWriteLock();
			}

			try
			{
				constructorDictionary[type] = invokeInfo;
			}
			finally
			{
				if (useLock)
				{
					constructorDictionaryLockSlim.ExitWriteLock();
				}
			}

			return Instantiate(invokeInfo);
		}

		static string GetKeyValueOrDefault(string key)
		{
			if (string.IsNullOrWhiteSpace(key))
			{
				key = defaultKey;
			}

			return key;
		}
	}
}
